/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved.
 *
 */

import os from 'os';
import path from 'path';

import { globalData, Json5Reader, Module, parseJsonFile } from '@ohos/hvigor';
import * as fs from 'fs-extra';
import fse from 'fs-extra';

import { ohosTrace } from '../../common/trace/ohos-trace.js';
import { BuildArtifactExtension } from '../../const/build-directory-const.js';
import { BuildProfileSchemaFileConst, CommonConst, DefaultTargetConst } from '../../const/common-const.js';
import { DeviceTypeConst } from '../../const/device-type-const.js';
import { CompileModeEnum } from '../../enum/compile-mode-enum.js';
import { ModuleType } from '../../enum/module-type-enum.js';
import { ModuleBuildProfile } from '../../options/build/module-build-profile.js';
import { ProjectBuildProfile } from '../../options/build/project-build-profile.js';
import { ConfigJson } from '../../options/configure/config-json-options.js';
import { ModuleJson } from '../../options/configure/module-json-options.js';
import { ApiType } from '../../project/data/hap-extra-info.js';
import { getElementFromArr } from '../../utils/array-util.js';
import { OhosLogger } from '../../utils/log/ohos-logger.js';
import { handleEntryModulesErrors } from '../../utils/validate/error-handlers.js';
import { ValidateUtil } from '../../utils/validate/validate-util.js';
import { ProjectModel } from '../project/project-model.js';
import { SourceSetModel, SourceSetModelType } from '../source-set/source-set-model.js';
import { ModuleModel } from './module-model.js';

import ConfigOptObj = ConfigJson.ConfigOptObj;

import { BuildOpt } from '../../options/build/build-opt.js';
import { FileUtil } from '../../utils/file-util.js';

import ProductApiMeta = ProjectBuildProfile.ProductApiMeta;
import ApiMeta = ProjectBuildProfile.ApiMeta;

import { AkrUIXConfigJson } from '../../options/configure/akruix-config-opt.js';
import { ModuleTaskService } from '../../tasks/service/module-task-service.js';
import { JsonProfile } from '../json-profile.js';
import { moduleBuildProfileLoader } from '../../utils/loader/file/module-build-profile-loader.js';
import { ohPackageLoader } from '../../utils/loader/file/oh-package-loader';
import { resModelLoader } from '../../utils/loader/file/res-model-loader.js';

const log = OhosLogger.getLogger('ohos-core-module-model');
const invalidBuildProfilePath: Set<string> = new Set();

/**
 * hvigor 工程的module模块的数据管理对象
 *
 * @since 2022/2/23
 */
export abstract class CoreModuleModelImpl implements ModuleModel {
  private readonly module: Module;
  private readonly name: string;
  private readonly parentProject: ProjectModel;
  private readonly buildProfilePath: string;
  private moduleBuildOpt: ModuleBuildProfile.ModuleBuildOpt;
  private belongProjectPath: string;
  private compilePluginPath: string | undefined = undefined;

  // module的所有target的源码根路径,{modulePath}/src/
  protected readonly _sourceRootDir: string;
  protected readonly modulePath: string;
  protected targetSourceSetMap: Map<string, SourceSetModelType> = new Map<string, SourceSetModelType>();
  protected targets: ModuleBuildProfile.ModuleTargetBuildOpt[];

  protected constructor(module: Module, parentProject: ProjectModel) {
    this.module = module;
    this.name = module.getName();
    this.modulePath = module.getNodeDir();
    this.parentProject = parentProject;
    this.buildProfilePath = path.resolve(this.modulePath, CommonConst.PROFILE_JSON5);
    this.checkModuleBuildProfile(this.buildProfilePath);

    this.moduleBuildOpt = moduleBuildProfileLoader.getBuildProfile(this.name);
    this._sourceRootDir = path.resolve(this.modulePath, 'src');
    this.targets = this.getOrDefaultTargets();

    this.belongProjectPath = this.initBelongProjectPath(this.modulePath);
    this.trace();
  }

  /**
   * 初始化belongProjectPath
   *
   * @param modulePath 模块路径
   */
  initBelongProjectPath(modulePath: string): string {
    // 如果当前模块路径包含工程路径则使用工程路径作为belongProjectPath
    if (modulePath.startsWith(this.parentProject.getProjectDir() + path.sep)) {
      return this.parentProject.getProjectDir();
    }

    // 如果是跨工程的模块则使用上级目录作为belongProjectPath
    return path.resolve(this.modulePath, '..');
  }

  findBelongProjectPath(currentDirPath: string): string {
    if (currentDirPath === path.dirname(currentDirPath)) {
      // 找不到所属的工程结构，使用引用当前模块的上一级目录作为belongProjectPath。
      return path.resolve(this.modulePath, '..');
    }

    // build-profile   modules   illegal return
    const buildProfilePath = path.resolve(currentDirPath, CommonConst.PROFILE_JSON5);
    if (invalidBuildProfilePath.has(buildProfilePath)) {
      return this.findBelongProjectPath(path.dirname(currentDirPath));
    }

    if (!fse.existsSync(buildProfilePath)) {
      invalidBuildProfilePath.add(buildProfilePath);
      return this.findBelongProjectPath(path.dirname(currentDirPath));
    }

    const buildProfileOpt = Json5Reader.getJson5Obj(buildProfilePath);
    if (buildProfilePath && buildProfileOpt.modules) {
      return currentDirPath;
    }

    invalidBuildProfilePath.add(buildProfilePath);
    return this.findBelongProjectPath(path.dirname(currentDirPath));
  }

  public getBelongProjectPath(): string {
    return this.belongProjectPath;
  }

  getCompilePluginPath(): string | undefined {
    return this.compilePluginPath;
  }

  public setCompilePluginPath(path: string) {
    this.compilePluginPath = path;
  }

  public refreshData(): void {
    this.moduleBuildOpt = moduleBuildProfileLoader.getBuildProfile(this.name);
    const newTargets = this.getOrDefaultTargets();
    newTargets.forEach((target) => {
      const oldTarget = this.targets.find((t) => t.name === target.name);
      if (oldTarget) {
        // 如果找到了对象，那么替换对象的内容
        // 使用Object.keys(target)来获取所有要替换的属性名
        for (const key of Object.keys(target)) {
          oldTarget[key] = target[key];
        }
      }
    });
    this.trace();
  }

  trace() {
    ohosTrace.traceModules(this.name, this.moduleBuildOpt.apiType);
  }

  isOhpmProject(): boolean {
    return this.parentProject.isOhpmProject();
  }

  abstract isAtomicService(): boolean;

  abstract isInstallFree(): boolean;

  abstract getDeviceTypes(): string[];

  abstract getPermission(): object;

  isMixedDeviceModule() {
    return (
      this.getDeviceTypes().some((deviceType) => DeviceTypeConst.RICH_DEVICES.includes(deviceType)) &&
      this.getDeviceTypes().some((deviceType) => DeviceTypeConst.LITE_DEVICES.includes(deviceType))
    );
  }

  isPurerRichDeviceModule() {
    return this.getDeviceTypes().every((deviceType) => DeviceTypeConst.RICH_DEVICES.includes(deviceType));
  }

  haveRichDevicesInModule() {
    return this.getDeviceTypes().some((deviceType) => DeviceTypeConst.RICH_DEVICES.includes(deviceType));
  }

  haveLiteDevicesInModule() {
    return this.getDeviceTypes().some((deviceType) => DeviceTypeConst.LITE_DEVICES.includes(deviceType));
  }

  isPurerLiteDeviceModule() {
    return this.getDeviceTypes().every((deviceType) => DeviceTypeConst.LITE_DEVICES.includes(deviceType));
  }

  isSingleDeviceTypeModule(): boolean {
    return this.isPurerRichDeviceModule() || this.isPurerLiteDeviceModule();
  }

  abstract getJsonObjByTargetName(targetName: string): ConfigOptObj | ModuleJson.ModuleOptObj;

  abstract initDefaultTargetSourceSet(): void;

  abstract getModuleType(): ModuleType;

  abstract getSourceSetByTargetName(targetName: string): SourceSetModel;

  abstract getJsonPathByTargetName(targetName: string): string;

  getRelatedEntryModules(): string[] | undefined {
    if (this.getModuleType() !== ModuleType.Feature) {
      return undefined;
    }

    const entryModules = this.getProfileOpt().entryModules;
    if (this.getApiType() === ApiType.FA && entryModules === undefined) {
      // FA模型的feature需要配置entryModules
      log.printErrorExit(
        'NO_ENTRY_MODULES_IN_FA_PROJECT',
        [],
        [[this.getProfilePath(), this.getName()]]
      );
    }

    // api9+&stage模型工程模板feature模块build-profile.json5中不再默认添加entryModules字段,该字段可选配
    return entryModules ?? undefined;
  }

  getSourceRootByTargetName(targetName = DefaultTargetConst.DEFAULT_TARGET): string {
    if (this.targetSourceSetMap.has(targetName)) {
      return this.targetSourceSetMap.get(targetName)!.getSourceSetRoot();
    }
    return this.targetSourceSetMap.get(DefaultTargetConst.DEFAULT_TARGET)!.getSourceSetRoot();
  }

  getBuildProfileName(): string {
    return CommonConst.PROFILE_JSON5;
  }

  getName(): string {
    return this.name;
  }

  getProjectDir(): string {
    return this.modulePath;
  }

  getParentProject(): ProjectModel {
    return this.parentProject;
  }

  getPackageJsonPath(): string {
    return path.resolve(this.modulePath, CommonConst.PACKAGE_JSON);
  }

  /**
   * 返回模块下的oh-package.json5路径 || .hvigor/dependencyMap下的oh-package.json5路径
   * @returns {string}
   */
  getOhPackageJson5Path(): string {
    return ohPackageLoader.getNodeOhPackagePath(path.resolve(this.modulePath, CommonConst.OH_PACKAGE_JSON5));
  }

  getProfilePath(): string {
    return this.buildProfilePath;
  }

  getProfileOpt(): ModuleBuildProfile.ModuleBuildOpt {
    return this.moduleBuildOpt;
  }

  isArkModule(): boolean {
    return true;
  }

  /**
   * 根据模块的运行时配置文件module.json5/config.json来获取取模块的类型信息
   *
   * @returns {boolean} true/false
   */
  isHapModule(): boolean {
    return this.getModuleType() === ModuleType.Entry || this.getModuleType() === ModuleType.Feature;
  }

  isHarModule(): boolean {
    return this.getModuleType() === ModuleType.Har;
  }

  isHspModule(): boolean {
    return this.getModuleType() === ModuleType.Shared;
  }

  getProductApiMeta(product: string): ProductApiMeta | undefined {
    return this.getParentProject().getProductApiMeta(product);
  }

  getCompileApiMetaByProduct(product: string): ApiMeta {
    const opt: ProductApiMeta | undefined = this.getProductApiMeta(product);
    if (!opt) {
      throw new Error(`Can not find product ${product}.`);
    }
    return opt.compileSdkVersion;
  }

  getCompatibleApiMetaByProduct(product: string): ApiMeta {
    const opt: ProductApiMeta | undefined = this.getProductApiMeta(product);
    if (!opt) {
      throw new Error(`Can not find product ${product}.`);
    }
    return opt.compatibleSdkVersion;
  }

  getTargetApiMetaByProduct(product: string): ProjectBuildProfile.ApiMeta | undefined {
    return this.getProductApiMeta(product)?.targetSdkVersion;
  }

  getAllModules(): ModuleModel[] {
    return this.parentProject.getAllModules();
  }

  getApiType(): ApiType {
    return this.moduleBuildOpt.apiType === undefined ? ApiType.STAGE : this.moduleBuildOpt.apiType;
  }

  /**
   * 模块的compileMode根据工程的属性进行判断
   *
   * @returns {CompileModeEnum} jsBundle | esModule
   */
  getCompileMode(product: ProjectBuildProfile.ProductBuildOpt): CompileModeEnum {
    const version: number = this.getCompileApiMetaByProduct(product.name).version;
    return this.getApiType() === ApiType.FA || version < 9 ? CompileModeEnum.JS_BUNDLE : CompileModeEnum.ES_MODULE;
  }

  getTargetOptions(): ModuleBuildProfile.ModuleTargetBuildOpt[] {
    return this.targets;
  }

  /**
   * 获取对应module的targets集合，如果没有配置，则默认为
   * [
   *  {
   *    name: 'default'
   *  }
   * ]
   *
   * @return {ModuleBuildProfile.ModuleTargetBuildOpt[]}
   */
  private getOrDefaultTargets(): ModuleBuildProfile.ModuleTargetBuildOpt[] {
    const targets = this.getProfileOpt().targets;
    if (targets === undefined) {
      return [
        {
          name: 'default',
        },
      ];
    }

    const targetNames = targets.map((target) => {
      return target.name;
    });
    if (!targetNames.includes('default')) {
      targets.push({
        name: 'default',
      });
    }
    return targets;
  }

  moduleBuildProfileCheck(isHapModule: boolean): void {
    // 这里没有直接调用isHarModule()的方法是因为其调用了getModuleType(),当前方法执行时,getModuleType()方法还未初始化
    // 当前阶段通过hvigorfile.ts中引用的插件类型来判断是hap还是har,后面有判断引用的插件类型跟getModuleType()状态是否一致的校验
    // 这里不能通过getPluginById()接口获取对应的plugin,因为执行到这的时候,plugin还没初始化完,因此通过参数传递
    const moduleSchemaPath: string = isHapModule
      ? BuildProfileSchemaFileConst.HAP_MODULE_BUILD_PROFILE_SCHEMA_PATH
      : BuildProfileSchemaFileConst.HAR_MODULE_BUILD_PROFILE_SCHEMA_PATH;

    ValidateUtil.submitSchemaCheckWork({
      moduleName: this.name,
      filePath: this.buildProfilePath,
      schemaPath: moduleSchemaPath,
      errorHandlers: [handleEntryModulesErrors],
    });
    ValidateUtil.validateDuplicatedName(this.getProfileOpt().targets, 'targets', this.buildProfilePath);

    this.validateCompileMode();
  }

  private checkModuleBuildProfile(buildProfilePath: string) {

    if (!fs.existsSync(buildProfilePath)) {
      log.printErrorExit(
        'BUILD_CONFIG_FILE_NOT_FOUND',
        [this.name, buildProfilePath ],
        [[buildProfilePath]]
      );
    }
  }

  private validateCompileMode() {
    const buildProfileOpt = parseJsonFile(this.buildProfilePath, true);
    const compileMode = buildProfileOpt?.buildOption?.compileMode;
    if (compileMode) {
      const line = compileMode._line;
      const column = compileMode._column + 1;
      log.warn(`compileMode has been deprecated. The value of it is invalid.${os.EOL}         at ${this.buildProfilePath}:${line}:${column}`);
    }
  }

  getModule(): Module {
    return this.module;
  }

  processTargetsConfig(): void {
    const defaultTarget = getElementFromArr(this.moduleBuildOpt.targets, 'default');
    const ohosTestTarget = getElementFromArr(this.moduleBuildOpt.targets, 'ohosTest');

    if (ohosTestTarget !== undefined && ohosTestTarget.runtimeOS === undefined) {
      ohosTestTarget.runtimeOS = defaultTarget?.runtimeOS;
    }
  }

  /**
   * 返回当前模块的绝对路径化的worker路径，校验移至prebuild完成
   *
   * @param {BuildOpt | undefined} buildOption
   * @returns {string[] | undefined}
   */
  getWorkerConfig(buildOption: BuildOpt | undefined): string[] | undefined {
    return buildOption?.sourceOption?.workers?.map((workerPath) => {
      return FileUtil.convertToAbsolutePath(workerPath, this.getProjectDir());
    });
  }

  getAllTargetSourceSet(): ReadonlyMap<string, SourceSetModelType> {
    return this.targetSourceSetMap as ReadonlyMap<string, SourceSetModelType>;
  }

  getJsonProfilePath(targetName: string, isFaMode = false) {
    const profileName = isFaMode ? CommonConst.CONFIG_JSON : CommonConst.MODULE_JSON5;
    const sourceSetRoot = this.getSourceSetByTargetName(targetName).getSourceSetRoot();
    return path.resolve(sourceSetRoot, profileName);
  }

  getJsonProfileOptByTargetName(targetName: string, isFaMode = false) {
    const jsonFilePath = this.getJsonProfilePath(targetName, isFaMode);
    return isFaMode ? resModelLoader.getConfigJson(jsonFilePath) : resModelLoader.getModuleJson(jsonFilePath);
  }

  /**
   * 通过模型获取jsonProfile
   *
   * @param service moduleTaskService
   * @param targetName targetName
   * @private
   */
  getJsonProfileByModel(service: ModuleTaskService, targetName: string): JsonProfile {
    const jsonFile = this.getJsonProfileOptByTargetName(targetName, service.isFaMode());
    const jsonFilePath = this.getJsonProfilePath(targetName, service.isFaMode());
    if (globalData.cliOpts._.includes(CommonConst.ON_DEVICE_TEST) && !jsonFile) {
      log.printErrorExit(
        'ON_DEVICE_TEST_FAIL',
        [jsonFilePath, CommonConst.ON_DEVICE_TEST],
        [[jsonFilePath]]
      );
    }

    return {
      jsonFilePath,
      profile: jsonFile,
      deviceTypes: service.isFaMode() ? (jsonFile as ConfigJson.ConfigOptObj).module.deviceType : (jsonFile as ModuleJson.ModuleOptObj).module.deviceTypes,
      deviceConfig: service.isFaMode() ? CommonConst.DEVICE_TYPE : CommonConst.DEVICE_TYPES,
      configurationProfile: service.isFaMode() ? CommonConst.CONFIG_JSON : CommonConst.MODULE_JSON5,
    };
  }

  isCrossplatformModule(arkUIXConfigObj: AkrUIXConfigJson.ConfigObj | undefined): boolean {
    if (!arkUIXConfigObj) {
      return false;
    }
    return arkUIXConfigObj.crossplatform && arkUIXConfigObj.modules?.includes(this.getModule().getName());
  }

  /**
   * 获取远程仓hsp路径
   */
  getRemoteHspPath(): string {
    return path.resolve(this.getProjectDir(), CommonConst.OH_MODULES, BuildArtifactExtension.DOT_HSP);
  }

  getMockConfigPath(): string {
    return path.join(this.modulePath, 'src', 'mock', CommonConst.MOCK_CONFIG_JSON5);
  }

  public isOutModule(): boolean {
    return this.parentProject.getProjectDir() !== this.belongProjectPath;
  }
}
